/*
 * $Id: svnfs.c 30 2007-06-18 21:45:21Z john $
 *
 *     SVN Filesystem
 *     Copyright (C) 2006 John Madden <maddenj@skynet.ie>
 *
 *     This program can be distributed under the terms of the GNU GPL.
 *     See the file COPYING for details.
*/

/* vim "+set tabstop=4 shiftwidth=4 expandtab" */

#ifdef linux
#define _XOPEN_SOURCE 500
#endif

#define FUSE_USE_VERSION 25

#ifdef HAVE_SETXATTR
#include <sys/xattr.h>
#endif

#include <sys/time.h>

#include "svnfs.h"
#include "svnclient.h"

#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <mhash.h>
#include <pthread.h>
#define HASH_DIGIT 5

#include "iniparser.h"

/* Debug logging */
void DEBUG(char *fmt, ...) {
    if( svnfs.debug ) {
        va_list ap;

        va_start(ap, fmt);
        printf("DEBUG : ");
        vprintf(fmt, ap);
        printf("\n");
    }
}
/* svnfs specific options */

#define SVNFS_OPT(a, b, c) { a, offsetof(struct svnfs, b), c }

static struct fuse_opt svnfs_opts[] = { 
    SVNFS_OPT( "debug", debug, 1 ),
    FUSE_OPT_END
};

/* helper function to print key in hash table */
void iterator(gpointer key, gpointer value, gpointer user_data) {
 printf(user_data, key);
}

/* Filesystem functions */

static int svnfs_getattr(const char *path, struct stat *buf) {
    int err;

    DEBUG("svnfs_getattr(): path : '%s'", path);

    memset(buf, 0, sizeof(struct stat));

    if( strcmp(path, "/") == 0 ) {
        buf->st_mode = S_IFDIR | 0755; /* 34236 */
        buf->st_nlink = 2;
        buf->st_uid = svnfs.uid;
        return(0);
    } 

    pthread_mutex_lock(&lock_dirbuf_hash);
    struct dirbuf *dp = g_hash_table_lookup(dirbuf_hash,path);
    pthread_mutex_unlock(&lock_dirbuf_hash);

    /* Need to check the repository - not in the cache */
    if (dp == NULL) {
       //DEBUG("svnfs_getattr(): has %d entries in attribute hash", g_hash_table_size(dirbuf_hash));
       //g_hash_table_foreach(dirbuf_hash, (GHFunc)iterator, " %s\n");

       // If the parent directory is already in list cache,
       //  no need to load.
       char *parent = strdup(path);
       char *slash_idx = rindex(parent,'/');
       if (slash_idx != NULL) {
           *slash_idx = '\0';
           //DEBUG("svnfs_getattr(): parent '%s'", parent);
           if (strlen(parent) == 0) { // root directory
              free(parent);
              DEBUG("svnfs_getattr(): skip unrecognized '%s'", path);
              return(-ENOENT);
           }
           pthread_mutex_lock(&lock_listbuf_hash);
           GSList *list = g_hash_table_lookup(listbuf_hash,parent);
           pthread_mutex_unlock(&lock_listbuf_hash);
           if (list != NULL) {
              free(parent);
              DEBUG("svnfs_getattr(): skip unrecognized '%s'", path);
              return(-ENOENT);
           }
       } 
       free(parent);

       DEBUG("svnfs_getattr(): load '%s' into attribute hash", path);
       if( (err = svnclient_list(path, dp)) ) {
           return(-err);
       }
       pthread_mutex_lock(&lock_dirbuf_hash);
       dp = g_hash_table_lookup(dirbuf_hash,path);
       pthread_mutex_unlock(&lock_dirbuf_hash);
       if (dp == NULL) {
          DEBUG("svnfs_getattr(): fail loading '%s' into attribute hash", path);
          return -1;
       }
    }

    DEBUG("svnfs_getattr(): found '%s' in attribute hash", path);
    buf->st_mode = dp->st.st_mode;
    buf->st_size = dp->st.st_size;
    buf->st_uid = svnfs.uid;
    buf->st_mtime = dp->st.st_mtime;
    buf->st_atime = dp->st.st_mtime;
    buf->st_ctime = dp->st.st_mtime;
    return(0);
}

/**
 * return local file descriptor; otherwise 
 */
static int get_local_fd(struct dirbuf *dp) {
    int fd = -1;
    int err = 0;

    unsigned char hash[20];   // for SHA1 hash
    MHASH td;
    uid_t uid;
    char cache_id[256];
    char subhash[HASH_DIGIT+1];
    int i,j;

    td = mhash_init(MHASH_SHA1);
    if (td == MHASH_FAILED) {
       return(-EFAULT);
    }
    mhash(td,svnfs.svnpath,strlen(svnfs.svnpath));
    mhash(td,dp->name,strlen(dp->name));
    mhash_deinit(td,hash);
    for (i=0,j=0; i < HASH_DIGIT; i++,j+=2) {
       sprintf(subhash+j,"%.2x",hash[i]);
    }
    uid = svnfs.uid;
    sprintf(cache_id,"/tmp/%d.%s.%d.%s",uid,rindex(dp->name,'/')+1,dp->rev,subhash);
    DEBUG("get_local_fd(): cache_id %s\n", cache_id);

    pthread_mutex_lock(&(dp->lock));
    fd = open(cache_id,O_RDONLY);
    if (fd == -1) {  // handle only file not found for now?
       if(err = svnclient_cache(dp,cache_id)) {
           return(-err);
       }
       fd = open(cache_id,O_RDONLY);
    }
    pthread_mutex_unlock(&(dp->lock));

    DEBUG("get_local_fd(): %s fd=%d", dp->name,fd);
    return fd; 

}

static int svnfs_open(const char *path, struct fuse_file_info *fi) {
    (void) fi;

    DEBUG("svnfs_open(): path : %s", path);

    pthread_mutex_lock(&lock_dirbuf_hash);
    struct dirbuf *dp = g_hash_table_lookup(dirbuf_hash,path);
    pthread_mutex_unlock(&lock_dirbuf_hash);
    if (dp == NULL) {
       return(-ENOENT);
    }

    fi->fh = get_local_fd(dp);
    if (fi->fh < 0) {
       return(-ENOENT);
    }

    return(0);
}

static int svnfs_read(const char *path, char *buf, size_t size, 
       off_t offset, struct fuse_file_info *fi) {
    int count = 0;

    DEBUG("svnfs_read(): %d from %s, offset %d", size, path, offset);
    count = pread(fi->fh,buf,size,offset);
    if (count == -1) {
       return -errno;
    }
    DEBUG("svnfs_read(): size %d", count);
    return(count);
}

static int svnfs_readdir(const char *path, void *buf, 
       fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi) {
    struct dirbuf *dp = NULL;
    int err;

    (void)fi;

    DEBUG("svnfs_readdir(): path : '%s'", path);

    
    pthread_mutex_lock(&lock_listbuf_hash);
    struct listbuf *lp = g_hash_table_lookup(listbuf_hash,path);
    pthread_mutex_unlock(&lock_listbuf_hash);
    
    if (lp == NULL) {
       DEBUG("svnfs_readdir(): load '%s' into list cache", path);
       if( (err = svnclient_list(path, dp)) ) {
           return(-err);
       }
       pthread_mutex_lock(&lock_listbuf_hash);
       lp = g_hash_table_lookup(listbuf_hash,path);
       pthread_mutex_unlock(&lock_listbuf_hash);
       if (lp == NULL) { 
          DEBUG("svnfs_readdir(): fail loading '%s' into list cache", path);
          //g_hash_table_foreach(listbuf_hash, (GHFunc)iterator, "'%s'\n");
          return -1;
       }
    }

    /* The directory list should already be populated in list cache */
    DEBUG("svnfs_readdir(): use '%s' from list cache", path);
    GSList *list = lp->list;
    GSList *iterator = NULL;

    filler(buf, ".", NULL, 0);
    filler(buf, "..", NULL, 0);
    for (iterator = list; iterator; iterator = iterator->next) {
        dp = iterator->data;
        if( strlen(path) > 1 ) { /* path is other than '/' */

            if( filler(buf, (rindex(dp->name, '/') + 1), &(dp->st), 0) )
                break;
        } else { /* path is '/' */
            if( strstr((dp->name + 1), "/") == NULL ) {
                if( filler(buf, (dp->name + 1), &(dp->st), 0) )
                    break;
            }
        }
    }

    return(0);
}

/**
 * This releases an open file. release() is called when there are no more 
 * references to an open file.
 */
static int svnfs_release(const char *path, struct fuse_file_info *fi) {
    DEBUG("svnfs_release(): %s", path);
    if (close(fi->fh) == -1) {
       return(-errno);
    }
    return 0;
}

/* End filesystem functions */

static struct fuse_operations svnfs_oper = {
    /*
    .mknod = svnfs_mknod,
    .mkdir = svnfs_mkdir,
    .unlink = svnfs_unlink,
    .rmdir = svnfs_rmdir,
    .symlink = svnfs_symlink,
    .rename = svnfs_rename,
    .link = svnfs_link,
    .chmod = svnfs_chmod,
    .truncate = svnfs_truncate,
    .write = svnfs_write,
    .readlink = svnfs_readlink,
    .chown = svnfs_chown,
    .utime = svnfs_utime,
    .statfs = svnfs_statfs,
    */
    .getattr = svnfs_getattr,
    .open = svnfs_open,
    .read = svnfs_read,
    .release = svnfs_release,
    .readdir = svnfs_readdir
};

void load_user_default_options() {
   struct stat buf;
   dictionary *dic;
 
   char *s;
   char *home;
   char *option_file;

   home = getenv("HOME");
   if (home == NULL) {
      return;
   }
   option_file = malloc(strlen(home)+20);
   sprintf(option_file,"%s/.svnfsrc",home);

   if (stat(option_file,&buf) < 0) {
      DEBUG("load_user_default_options(): cannot stat %s, ignore",option_file);
      return;
   }

   mode_t danger_mode = S_IRWXG | S_IRWXO;
   if (buf.st_mode & danger_mode) {
      fprintf(stderr,"File %s is not in secure mode (600), exit.\n",option_file);
      exit(1);
   }

   dic = iniparser_load(option_file);
   if (dic == NULL) {
      DEBUG("load_user_default_options(): fail to load %s, ignore",option_file);
      return;
   }
   DEBUG("load_user_default_options(): %s found",option_file);

   s = iniparser_getstring(dic,":username",NULL);
   if (s != NULL) {
      svnfs.username = strdup(s);
   } else {
      svnfs.username = strdup(cuserid(NULL));
   }
   s = iniparser_getstring(dic,":password",NULL);
   if (s != NULL) {
      svnfs.password = strdup(s);
   }
   s = iniparser_getstring(dic,":revision",NULL);
   if (s != NULL) {
      svnfs.revision = atoi(s);
   }

   iniparser_freedict(dic);
   free(option_file);
}

void usage(const char *progname) {
    printf(
"Usage: %s repository_url mountpoint [options]\n"
"\n"
"Options\n"
"   -o username=xxxx     set username\n"
"   -o password=xxxx     set password\n"
"   -o revision=nnn      mount to the revision other than head\n"
"                          if not specified, use the head revision and use the revision\n"
"                          found at mount time until unmounted for better consistency\n"
"   -f                   run in svnfs debug mode\n"
"   -d                   run in fuse debug mode\n"
"Files\n"
"   ~/.svnfsrc           default user options\n"
"   /tmp/uid.filename.rev.hash    cached file\n"
"\n",progname);

}

/**
 * Check if the SVN repository is connected.
 * @return 0 if connected.
 */
int svn_check_root() {
    struct dirbuf *dp = NULL;
    int err;

    if( (err = svnclient_list("/", dp)) ) {
        DEBUG("svn_check_root(): fails %d",err);
        return(-err);
    }
    DEBUG("svn_check_root(): OK, connect to revision %d", svnfs.revision);

    return 0;
}

/**
 * Check if arg is key=value
 * @return value
 */
char *get_arg_value(const char *arg, const char *key) {
    int key_len = strlen(key);
    char *value;
    if ( (strncmp(arg,key,key_len) == 0) && (arg[key_len] == '=') ) {
       value = strdup(arg + key_len + 1); 
       return value;
    }
    return NULL;
}

int svnfs_parse_opts(void *data, const char *arg, int key, 
       struct fuse_args *outargs) {
    char *value;

    switch(key) {
        case FUSE_OPT_KEY_OPT:
            /* svnfs specific options */
            if ( (value=get_arg_value(arg,"username")) != NULL) {
               svnfs.username = strdup(value);
               return 0;
            }
            if ( (value=get_arg_value(arg,"password")) != NULL) {
               svnfs.password = strdup(value);
               return 0;
            }
            if ( (value=get_arg_value(arg,"revision")) != NULL) {
               svnfs.revision = atoi(value);
               return 0;
            }
            break;
       
        case FUSE_OPT_KEY_NONOPT:
            if( !svnfs.svnpath ) {
                svnfs.svnpath = strdup(arg);
                return(0);
            }
            return(1);

        default :
            fprintf(stderr, "Error, unknown option %s\n", arg);
            return(1);
    }

    /* Silence compiler warning about reaching end of non-void function */
    return(1);
}

int main(int argc, char *argv[]) {

    int i;
    struct fuse_args args = FUSE_ARGS_INIT(argc, argv);

    svnfs.debug = 0;
    svnfs.username = "";
    svnfs.password = "";
    svnfs.revision = 0;
    svnfs.untrusted = 1;

    // Trying to set debug option but is there a way to get it directly from fuse?
    for (i=1; i < argc; i++) {
       if (strcmp("-f",argv[i]) == 0) {
          svnfs.debug = 1;
          break;
       }   
    }

    load_user_default_options();

    if( fuse_opt_parse(&args, &svnfs, svnfs_opts, svnfs_parse_opts) == -1 ) {
        fprintf(stderr, "Error parsing command line arguments\n");
        exit(1);
    }

    if( !svnfs.svnpath ) {
        fprintf(stderr, "No Subversion repository given\n");
        usage(argv[0]);
        exit(1);
    }
    while( svnfs.svnpath[strlen(svnfs.svnpath)-1] == '/' )
        svnfs.svnpath[strlen(svnfs.svnpath)-1] = '\0';

    gettimeofday(&svnfs.mnttime, NULL);
    svnfs.uid = getuid();

    DEBUG("struct svnfs = {");
    DEBUG("\tdebug = %d", svnfs.debug);
    DEBUG("\tsvnpath = %s", svnfs.svnpath);
    DEBUG("\tusername = %s", svnfs.username);
    DEBUG("\tpassword = %s", svnfs.password);
    if (svnfs.revision > 0) {
       DEBUG("\trevision = %d", svnfs.revision);
    } else {
       DEBUG("\trevision = head");
    }
    DEBUG("\tuntrusted = %d", svnfs.untrusted);
    DEBUG("\tmnttime.tv_sec = %d", svnfs.mnttime.tv_sec);
    DEBUG("}");

    if( svnclient_setup_ctx() ) {
        exit(1);
    }

    if (svn_check_root() != 0) {
        fprintf(stderr, "Fail to connect to %s\n", svnfs.svnpath);
        exit(1);
    }

    return(fuse_main(args.argc, args.argv, &svnfs_oper));
}
